package org.test4j.mock.processor.filer;

import com.squareup.javapoet.*;
import org.test4j.mock.faking.fluent.MocksApply;
import org.test4j.mock.processor.MockProcessor;
import org.test4j.mock.processor.filer.file.MockClassFiler;
import org.test4j.mock.processor.filer.file.MockTypeFiler;
import org.test4j.mock.processor.filer.file.MockUpFiler;

import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import java.util.*;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.function.Consumer;

import static org.test4j.mock.processor.MockProcessor.error;
import static org.test4j.mock.processor.filer.ClassNames.MockUp_Suffix;
import static org.test4j.mock.processor.filer.ClassNames.Mock_Package_WithDot;

public class MocksFiler {
    private static Set<String> Has_Mocks = new ConcurrentSkipListSet<>();
    private ClassName fullName;
    private String suffix;
    private Map<String, ClassName> mocks = new HashMap<>();

    public MocksFiler(String fullName, String suffix, List<String> mocks) {
        this.fullName = ClassNames.getClassName(fullName);
        this.suffix = suffix;
        this.addToMockClass(mocks);
    }

    private void addToMockClass(Collection<String> mocks) {
        for (String mock : mocks) {
            this.addMockFile(mock);
        }
    }

    private void addMockFile(String mock) {
        if (mock == null || mock.isEmpty()) {
            return;
        }
        ClassName mockClassName = ClassNames.getClassName(Mock_Package_WithDot + mock + MockUp_Suffix);
        this.mocks.put(mock, mockClassName);
    }

    public void writeFiler() {
        TypeSpec.Builder builder = TypeSpec.classBuilder(this.fullName.simpleName() + this.suffix)
            .addModifiers(Modifier.PUBLIC)
            .superclass(MocksApply.class);

        this.build(builder);
        JavaFile.Builder javaBuilder = JavaFile.builder(this.fullName.packageName(), builder.build());
        MockProcessor.writeFiler(javaBuilder.build());
    }

    private void build(TypeSpec.Builder builder) {
        for (Map.Entry<String, ClassName> entry : this.mocks.entrySet()) {
            builder.addMethod(this.m_mockUp1(entry.getKey(), entry.getValue()));
            builder.addMethod(this.m_mockUp2(entry.getKey(), entry.getValue()));
        }
        while (!this.mocks.isEmpty()) {
            Iterator<Map.Entry<String, ClassName>> it = this.mocks.entrySet().iterator();
            Map.Entry<String, ClassName> entry = it.next();
            it.remove();
            MockUpFiler mockUpFiler = this.generateMockUp(entry.getKey());
            if (mockUpFiler == null) {
                continue;
            }
            mockUpFiler.writeFiler();
            this.addMockFile(mockUpFiler.getSuperClass());
        }
    }

    private MethodSpec m_mockUp1(String toFaked, ClassName mockUp) {
        ClassName name = ClassNames.getClassName(toFaked);
        return MethodSpec.methodBuilder(name.simpleName())
            .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
            .addJavadoc("mock {@link $T}", name)
            .addParameter(ParameterizedTypeName.get(ClassName.get(Consumer.class), mockUp), "faker")
            .addParameter(ArrayTypeName.of(Object.class), "targets")
            .varargs(true)
            .returns(TypeVariableName.get(this.fullName.simpleName() + this.suffix))
            .addStatement("return super.apply(faker, new $T(targets))", mockUp)
            .build();
    }

    private MethodSpec m_mockUp2(String toFaked, ClassName mockUp) {
        ClassName name = ClassNames.getClassName(toFaked);
        return MethodSpec.methodBuilder(name.simpleName())
            .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
            .addJavadoc("mock {@link $T}", name)
            .addParameter(ArrayTypeName.of(Object.class), "targets")
            .varargs(true)
            .returns(mockUp)
            .addStatement("return new $T(targets)", mockUp)
            .build();
    }

    private MockUpFiler generateMockUp(String className) {
        if (Has_Mocks.contains(className)) {
            return null;
        }
        Has_Mocks.add(className);
        try {
            return new MockClassFiler(Class.forName(className));
        } catch (ClassNotFoundException e) {
            TypeElement typeElement = MockProcessor.getTypeElement(className);
            if (typeElement == null) {
                error("ClassNotFoundException", e);
            } else {
                return new MockTypeFiler(typeElement);
            }
        }
        return null;
    }
}